在 上一篇 中,我們已經介紹了傳統 callback 的缺點,以及 Promise 是如何克服這些缺點的。
而這篇將專注於解釋 Promise 的意義以及用法。
Promise,如字面的意思就代表 承諾。
當你拿到一個 Promise 的時候,代表在未來中這個 Promise 可能會有幾種狀況發生
承諾 被兌現 (fulfilled)
用 resolve()
來兌現
承諾 被打破 (rejected)
用 reject()
來表示失敗
承諾 一直沒有回應 (pending)
一直沒有回傳
也就是說,承諾代表的不見得是成功。
而根據這三種結果,我們接下來的動作會有所不同,如果:
承諾被兌現 就 繼續做預定好的下一件事
使用 .then()
承諾被打破 就 根據這個原因去做對應的動作
使用 .catch()
,或是 .then
的第二個參數
承諾 一直都沒有回應 就 繼續等下去
首先要先了解到底要怎麼創建一個 Promise。
創建 Promise 很簡單,只要 new
一個 Promise
物件出來即可:
var a = new Promise(function(resolve, reject) {
// sync or async codes
// if success, resolve
// if fail, reject
});
Promise
可以帶入一個函式,代表著要給予承諾的程式。
這個函式會被 Promise 傳入兩個參數,這兩個參數也是函式,他們分別代表:
兌現 (fulfilled)
通常以 resolve
命名該參數
當我們完成動作時,就呼叫 resolve()
來兌現我們的承諾
拒絕 (rejected)
通常以 reject
命名該參數
當我們動作失敗時,就呼叫 reject()
來打破我們的承諾
接下來讓我們看看要怎麼兌現承諾,以及兌現之後要怎麼接續動作:
var a = new Promise(function(resolve, reject) {
setTimeout(function(){
resolve('hello world');
}, 1000);
});
a.then(function(value) {
console.log(a); // Promise {<resolved>: "hello world"}
console.log(value + '1'); // "hello world1"
});
a.then(function(value) {
console.log(a); // Promise {<resolved>: "hello world"}
console.log(value + '2'); // "hello world2"
});
console.log(a); // Promise {<pending>}
先來看我們非同步的函式 setTimeout
這邊。很清楚的可以看到,這個非同步會在 1000
毫秒後 兌現承諾 ( 因為呼叫了 resolve()
)。
那等待兌現承諾著這一秒期間發生了什麼事情呢?讓我們往後面的程式碼看。我們緊接著會看到兩句 a.then
,還記得前面說過的,.then()
是代表承諾被實現之後才會去執行的,因此在這一秒內,我們都不會看到 a.then
會印出東西。
接著我們在看最後一句:console.log(a)
,這句並沒有被 then
所包住,因此這並不會等待 Promise 完成後才執行,就像 等餐的時候我們還是可以滑手機 一樣,console.log(a)
這句就代表滑手機這個動作,因為它與 Promise 兌現與否不相關,所以可以先做。此時印出 a
會發現他的值是 "Promise {<pending>}"
,這就代表承諾還沒有回應,等待中。
一秒到了,a.then
就會緊接著被執行。可以注意到的是,a
的值是可以保留下來的,因此無論你接了幾個 then
,他都可以偵測到 a
這個承諾的完成狀況。因此我們接著就會印出 Promise {<resolved>: "hello world1"}
、"hello world1"
與 Promise {<resolved>: "hello world2"}
、"hello world2"
兩組。可以看到此時 a
的值變成了 Promise {<resolved>: "hello world"}
,這代表此承諾已經被兌現,而兌現後他會給我們 "hello world"
這個值。
有了這個概念後,了解如何接收被拒絕的 Promise 就很簡單了
var a = new Promise(function(resolve, reject) {
setTimeout(function(){
reject('OOPS');
}, 1000);
});
a.catch(function(value) {
console.log(a); // Promise {<reject>: "OOPS"}
console.log(value); // "OOPS"
});
我們的非同步程式邏輯不變,只把 resolve
改成 reject
。
之後,我們使用 .catch
來抓住錯誤。過了一秒後,我們就可以得到 Promise
拒絕承諾的訊息了。
最後的 pending 就最好示範了。
當我們並沒有成功 resolve
或是 reject
的時候,就會 pending,也就是說 .then
內的程式會一直等待,而 .catch
也不會抓到任何錯誤。
var a = new Promise(function(resolve, reject) {});
console.log(a); // Promise {<pending>}
雖然這個例子有點刻意,但是卻都很容易出現在日常中。像是當我們試圖去連到一個網站,而他卻一直沒有回應時,我們的 Promise 就會一直處於 pending 狀態。
當你可以等待一個非同步動作完成後,再去執行下一個動作時,那接著你可能就會想說,如果有很多非同步動作要串接在一起的話要怎麼辦呢?
很幸運的,.then
可以串接非同步程式。講得更精確一點是,.then
不論是非同步或者同步的程式都可以串接,而 then
會回傳一個 Promise
。既然他又會回傳一個 Promise
就代表我們可以串接非同步程式。
也許你會很疑惑的問,假如我們的 .then
執行的是一個同步程式的話,那他還會回傳 Promise
物件嗎 ( 畢竟非同步才需要承諾 )?答案是會,.then
會把我們的 return
值包裹成一個 Promise
,並回傳回去。
讓我們來看看範例:
asyncFn()
.then(syncFn)
.then(asyncFn);
function asyncFn(data) {
return new Promise(function(resolve, reject) {
console.log('Async received data:', data);
setTimeout(function(){
resolve('async fn');
}, 1000);
});
}
function syncFn(data) {
console.log('Sync received data:', data);
return 'sync fn';
}
// "Async received data: undefined"
// "Sync received data: async fn"
// "Async received data: sync fn"
在這裡,我們先使用 asyncFn
回傳一個 Promise 回去。一秒後,syncFn
收到了來自 asyncFn
的兌現的承諾值 "async fn"
,在印出 "Sync received data: async fn"
之後,便 return 'sync fn'
。接著,JavaScript 會把 'sync fn'
包裹成一個 Promise
,因此我們可以再繼續 Chain 到下一個 .then
執行 asyncFn
。
這就證明了不論是非同步或者同步的程式都可以串接,而 then
會回傳一個 Promise
好讓我們繼續串接。
從這篇中,我們可以學到 Promise 代表的意思就是 承諾,承諾的未來有幾種可能:
承諾 被兌現 (fulfilled)
用 resolve()
來兌現
承諾 被打破 (rejected)
用 reject()
來表示失敗
承諾 一直沒有回應 (pending)
一直沒有回傳
而在這些承諾的未來之後,如果:
承諾被兌現 就 繼續做預定好的下一件事
使用 .then()
承諾被打破 就 根據這個原因去做對應的動作
使用 .catch()
,或是 .then
的第二個參數
承諾 一直都沒有回應 就 繼續等下去
實際上 Promise 還有一些 API 可以供我們使用,這部分可以參照 MDN 去更深入了解。
You Don't Know JS: async & performance
https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Promise
感謝你把 promise 深入淺出地解釋了一番,剛開始接觸真的不好理解。
不過這裡可能有點筆誤:
( 因為呼叫了
reject()
)
這裡應該是呼叫了 resolve()
吧
謝謝你指出錯誤,已修正~